Typical enterprise systems are build on a multi-tiered system. There are usually at least three tiers:
Presentation
Business
Integration
There might be a few more, but for now this list of tiers will suite us well.
Our first two tests produced Data Access Objects (dao)'s. These two dao's hide the details of getting books and patrons. They fall under the integration layer.
Now it is time to add a higher-level concept, the Library. The Library class represents a Facade to the underlying system. This so-called facade will be the primary interface to the middle tier of our system.
Of course, along the way we'll end up doing yet more refactoring to accommodate this new suite of tests.
Library
First we'll start with a new suite of tests for this Library facade. For this first pass, we'll write several basic tests and a few tests that move us closer to use-case like functionality.
Adding a Book
@Test
publicvoid addBook(){finalBook b = createBook();Set<Author> authors = b.getAuthors();finalBook found = library.findBookById(b.getId());
assertTrue(found.getAuthors().containsAll(authors));}privateBook createBook(){final Author a1 = new Author(newName("Christian", "Bauer"));final Author a2 = new Author(newName("Gavin", "King"));return library.createBook("Hibernate In Action", ISBN, Calendar
.getInstance().getTime(), a1, a2);}
Lookup a Book that Does Not Exist
Notice that this test has different results than the same test in the BookDaoTest. In this case we expect an exception to be thrown while in the case of the BookDaoTest we just get back null. Why? The dao has no way of knowing what the policy should be regarding not finding objects, whereas the Library facade can set the policy.
@Test
publicvoid addPatron(){final Patron p = createPatron();final Patron found = library.findPatronById(p.getId());
assertNotNull(found);}private Patron createPatron(){final Address a = new Address("5080 Spectrum Drive", "", "Dallas",
"TX", "75001");return library.createPatron(PATRON_ID, "Brett", "Schuchert",
"555-1212", a);}
Lookup a Patron that Does Not Exist
As with the BookDao, the PatronDao simply returns null if an object is not found by ID. The Library changes that null result into an exception.
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookThatDoesNotExist(){final Patron p = createPatron();
library.checkout(p.getId(), ID_DNE);}
Checkout a Book to a Patron that Does Not Exist
@Test(expected = EntityNotFoundException.class)publicvoid checkoutBookToPatronThatDoesNotExist(){finalBook b = createBook();
library.checkout(ID_DNE, b.getId());}
LibraryTest.java
Here's the shell of the test.
packagesession;importstaticorg.junit.Assert.assertEquals;importstaticorg.junit.Assert.assertFalse;importstaticorg.junit.Assert.assertNotNull;importstaticorg.junit.Assert.assertTrue;importjava.util.Calendar;importjava.util.Set;importjavax.persistence.EntityNotFoundException;importorg.junit.Before;importorg.junit.Test;importentity.Address;importentity.Author;importentity.Book;importentity.Name;importentity.Patron;importexception.BookAlreadyCheckedOut;publicclass LibraryTest extends EntityManagerBasedTest {privatestaticfinallong ID_DNE = -443123222l;privatestaticfinalString PATRON_ID = "113322";privatestaticfinalString ISBN = "1-932394-15-X";private Library library;
@Before
publicvoid setupLibrary(){final BookDao bd = new BookDao();
bd.setEm(getEm());final PatronDao pd = new PatronDao();
pd.setEm(getEm());
library = new Library();
library.setBookDao(bd);
library.setPatronDao(pd);}}
EntityManagerBasedTest
This new class inherits from a new base class called EnttyManagerBasedTest. This class factors out just the part of initialization related to the entity manager and the transactions from the BaseDbDaoTest.
packagesession;importjavax.persistence.EntityManager;importjavax.persistence.EntityManagerFactory;importjavax.persistence.Persistence;importorg.apache.log4j.BasicConfigurator;importorg.apache.log4j.Level;importorg.apache.log4j.Logger;importorg.junit.After;importorg.junit.Before;importorg.junit.BeforeClass;/**
* Our tests use an entity manager. The first pass at the BaseDbDaoTest forced
* initialization of a Dao. That works for the dao-based tests but not all
* tests. This class factors out just the part that sets up and cleans up the
* entity manager.
*
*/publicabstractclass EntityManagerBasedTest {private EntityManagerFactory emf;private EntityManager em;/**
* Once before the tests start running for a given class, init the logger
* with a basic configuration and set the default reporting layer to error
* for all classes whose package starts with org.
*/
@BeforeClass
publicstaticvoid initLogger(){// Produce minimal output.
BasicConfigurator.configure();// Comment this line to see a lot of initialization// status logging.Logger.getLogger("org").setLevel(Level.ERROR);}/**
* Before each test method, look up the entity manager factory, then create
* the entity manager.
*/
@Before
publicvoid initEmfAndEm(){
emf = Persistence.createEntityManagerFactory("lis");
em = emf.createEntityManager();
em.getTransaction().begin();}/**
* After each test method, roll back the transaction started in the -at-
* Before method then close both the entity manager and entity manager
* factory.
*/
@After
publicvoid closeEmAndEmf(){
getEm().getTransaction().rollback();
getEm().close();
emf.close();}public EntityManager getEm(){return em;}publicvoid setEm(EntityManager em){this.em = em;}}
BaseDbDaoTest
Here is yet another updated BaseDbDaoTest that reflects the new base class.
packagesession;importorg.junit.Before;/**
* A base class for tests that handles logger initialization, entity manager
* factory and entity manager creation, associating an entity manager with a
* dao, starting and rolling back transactions.
*/publicabstractclass BaseDbDaoTest extends EntityManagerBasedTest {/**
* Derived class is responsible for instantiating the dao. This method gives
* the hook necessary to this base class to init the dao with an entity
* manger in a per-test setup method.
*
* @return The dao to be used for a given test. The type specified is a base
* class from which all dao's inherit. The test derived class will
* override this method and change the return type to the type of
* dao it uses. This is called **covariance**. Java 5 allows
* covariant return types. I.e. BookDaoTest's version of getDao()
* will return BookDao while PatronDao's version of getDao() will
* return Patron.
*/publicabstract BaseDao getDao();/**
* The -at- before method in the base class executes first. After that, init
* the dao with the entity manager.
*/
@Before
publicvoid initDao(){
getDao().setEm(getEm());}}
The Exception
We've added one new unchecked exception to our system, BookAlreadyCheckedOut. Here it is:
packageexception;/**
* A simple unchecked exception reflecting a particular business rule violation.
* A book cannot be checked out if it is already checked out.
*
* This exception inherits from RuntimeException (or it is an unchecked
* exception). Why? The policy of whether to use checked or unchecked exceptions
* is project dependent. We are using this for learning about EJB3 and JPA and
* NOT about how to write exceptions, so using one policy versus the other is
* arbitrary for our purposes. Working with unchecked exceptions is a bit looser
* but also keeps the code looking a bit cleaner, so we've gone with unchecked
* exceptions.
*/publicclass BookAlreadyCheckedOut extendsRuntimeException{privatestaticfinallong serialVersionUID = 2286908621531520488L;finalLong bookId;public BookAlreadyCheckedOut(finalLong bookId){this.bookId = bookId;}publicLong getBookId(){return bookId;}}
Library
This class is all new.
packagesession;importjava.util.Date;importjava.util.List;importjavax.persistence.EntityNotFoundException;importentity.Address;importentity.Author;importentity.Book;importentity.Patron;importexception.BookAlreadyCheckedOut;publicclass Library {private BookDao bookDao;private PatronDao patronDao;public BookDao getBookDao(){return bookDao;}publicvoid setBookDao(BookDao bookDao){this.bookDao = bookDao;}public PatronDao getPatronDao(){return patronDao;}publicvoid setPatronDao(PatronDao patronDao){this.patronDao = patronDao;}publicBook createBook(finalString title, finalString isbn,
finalDate date, final Author a1, final Author a2){return getBookDao().create(title, isbn, date, a1, a2);}publicList<Book> findBookByIsbn(String isbn){return getBookDao().findByIsbn(isbn);}public Patron createPatron(finalString patronId, finalString fname,
finalString lname, finalString phoneNumber, final Address a){return getPatronDao().createPatron(fname, lname, phoneNumber, a);}public Patron findPatronById(finalLong id){final Patron p = getPatronDao().retrieve(id);if(p == null){thrownew EntityNotFoundException(String.format("Patron with id: %d does not exist", id));}return p;}publicvoid checkout(finalLong patronId, finalLong bookId){finalBook b = findBookById(bookId);if(b.isOnLoan()){thrownew BookAlreadyCheckedOut(bookId);}final Patron p = findPatronById(patronId);
p.addBook(b);
b.setBorrowedBy(p);
getPatronDao().update(p);}publicBook findBookById(Long id){finalBook b = getBookDao().findById(id);if(b == null){thrownew EntityNotFoundException(String.format("Book with Id:%d does not exist", id));}return b;}publicvoid returnBook(Long id){finalBook b = getBookDao().findById(id);if(b.isOnLoan()){final Patron p = b.checkin();
p.removeBook(b);
getPatronDao().update(p);}}}
BookDao
The tests use the findByIsbn() method, which returns a collection of Books. Why does findByIsbn() return a collection of books? The isbn is not unique; the book id is the only unique column. If we enforced a unique isbn, then there could only be one book of a given isbn in the library.
We've also added a method, findById, which should return a unique value (or null).
We need a basic utility to assist with equality. This utility will handle when we have null references.
EqualsUtil
packageutil;/**
* We typically need to compare two object and also perform null checking. This
* class provides a simple wrapper to accomplish doing so.
*/publicclass EqualsUtil {private EqualsUtil(){// I'm a utility class, do not instantiate me}publicstaticboolean equals(finalObject lhs, finalObject rhs){return lhs == null&& rhs == null
|| (lhs != null&& rhs != null&& lhs.equals(rhs));}}
Book
The book is somewhat changed. First it needs to import util.EqualsUtil (as shown below). It also contains some named queries and three new methods: isOnLoanTo, isOnLoan and checkin. The code below shows these changes.
importutil.EqualsUtil;/**
* I represent a Book. I have one named query to find a book by its isbn number.
* I also have a many to many relationship with author. Since I define the
* mappedBy, I'm the (arbitrarily picked) master of the relationship. I also
* take care of cascading changes to the database.
*/
@Entity/**
* A named query must have a globally unique name. That is why these are named
* "Book."... These queries could be associated with any entity. Given that they
* clearly deal with books, it seems appropriate to put them here. These will
* probably be pre-compiled and in any case available from the entity manager by
* using em.getNamedQuery("Book.findById").
*/
@NamedQueries({
@NamedQuery(name = "Book.findById",
query = "SELECT b FROM Book b where b.id = :id"),
@NamedQuery(name = "Book.findByIsbn",
query = "SELECT b FROM Book b WHERE b.isbn = :isbn")})publicclassBook{publicboolean isOnLoanTo(final Patron foundPatron){return EqualsUtil.equals(getBorrowedBy(), foundPatron);}publicboolean isOnLoan(){return getBorrowedBy()!= null;}public Patron checkin(){final Patron p = getBorrowedBy();
setBorrowedBy(null);return p;}}
Patron
There was only one change to Patron. We want to be able to ask the Patron if it is in fact borrowing a particular book.
There might be a few more, but for now this list of tiers will suite us well.
Our first two tests produced Data Access Objects (dao)'s. These two dao's hide the details of getting books and patrons. They fall under the integration layer.
Now it is time to add a higher-level concept, the Library. The Library class represents a Facade to the underlying system. This so-called facade will be the primary interface to the middle tier of our system.
Of course, along the way we'll end up doing yet more refactoring to accommodate this new suite of tests.
Library
First we'll start with a new suite of tests for this Library facade. For this first pass, we'll write several basic tests and a few tests that move us closer to use-case like functionality.Adding a Book
@Test public void addBook() { final Book b = createBook(); Set<Author> authors = b.getAuthors(); final Book found = library.findBookById(b.getId()); assertTrue(found.getAuthors().containsAll(authors)); } private Book createBook() { final Author a1 = new Author(new Name("Christian", "Bauer")); final Author a2 = new Author(new Name("Gavin", "King")); return library.createBook("Hibernate In Action", ISBN, Calendar .getInstance().getTime(), a1, a2); }Lookup a Book that Does Not Exist
Notice that this test has different results than the same test in the BookDaoTest. In this case we expect an exception to be thrown while in the case of the BookDaoTest we just get back null. Why? The dao has no way of knowing what the policy should be regarding not finding objects, whereas the Library facade can set the policy.
Adding a Patron
@Test public void addPatron() { final Patron p = createPatron(); final Patron found = library.findPatronById(p.getId()); assertNotNull(found); } private Patron createPatron() { final Address a = new Address("5080 Spectrum Drive", "", "Dallas", "TX", "75001"); return library.createPatron(PATRON_ID, "Brett", "Schuchert", "555-1212", a); }Lookup a Patron that Does Not Exist
As with the BookDao, the PatronDao simply returns null if an object is not found by ID. The Library changes that null result into an exception.
Checking out a book to a patron
@Test public void checkoutBook() { final Book b = createBook(); final Patron p = createPatron(); library.checkout(p.getId(), b.getId()); final Book foundBook = library.findBookById(b.getId()); final Patron foundPatron = library.findPatronById(p.getId()); assertTrue(foundBook.isOnLoanTo(foundPatron)); assertTrue(foundPatron.isBorrowing(foundBook)); }Returning a book
@Test public void returnBook() { final Book b = createBook(); final Patron p = createPatron(); library.checkout(p.getId(), b.getId()); final int booksBefore = p.getBorrowedBooks().size(); assertTrue(b.isOnLoan()); library.returnBook(b.getId()); assertEquals(booksBefore - 1, p.getBorrowedBooks().size()); assertFalse(b.isOnLoan()); }Returning a book that is not checked out
@Test public void returnBookThatsNotCheckedOut() { final Book b = createBook(); assertFalse(b.isOnLoan()); library.returnBook(b.getId()); assertFalse(b.isOnLoan()); }Checking out a Book that is Already Checked Out
Checkout a Book that Does Not Exist
Checkout a Book to a Patron that Does Not Exist
LibraryTest.java
Here's the shell of the test.
EntityManagerBasedTest
This new class inherits from a new base class called EnttyManagerBasedTest. This class factors out just the part of initialization related to the entity manager and the transactions from the BaseDbDaoTest.
BaseDbDaoTest
Here is yet another updated BaseDbDaoTest that reflects the new base class.
The Exception
We've added one new unchecked exception to our system, BookAlreadyCheckedOut. Here it is:Library
This class is all new.
BookDao
The tests use the findByIsbn() method, which returns a collection of Books. Why does findByIsbn() return a collection of books? The isbn is not unique; the book id is the only unique column. If we enforced a unique isbn, then there could only be one book of a given isbn in the library.
We've also added a method, findById, which should return a unique value (or null).
Util
We need a basic utility to assist with equality. This utility will handle when we have null references.EqualsUtil
EqualsUtilTest
Entity Changes
Book
The book is somewhat changed. First it needs to import util.EqualsUtil (as shown below). It also contains some named queries and three new methods: isOnLoanTo, isOnLoan and checkin. The code below shows these changes.
Patron
There was only one change to Patron. We want to be able to ask the Patron if it is in fact borrowing a particular book.